/*
 * CCVisu is a tool for visual graph clustering
 * and general force-directed graph layout.
 * This file is part of CCVisu.
 *
 * Copyright (C) 2005-2012  Dirk Beyer
 *
 * CCVisu is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * CCVisu is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with CCVisu; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 * Please find the GNU Lesser General Public License in file
 * license_lgpl.txt or http://www.gnu.org/licenses/lgpl.txt
 *
 * Dirk Beyer    (firstname.lastname@uni-passau.de)
 * University of Passau, Bavaria, Germany
 */
package org.sosy_lab.ccvisu.writers;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Stroke;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.TreeSet;

import org.sosy_lab.ccvisu.DisplayCriteria;
import org.sosy_lab.ccvisu.NameHandler;
import org.sosy_lab.ccvisu.Options;
import org.sosy_lab.ccvisu.Options.OptionsEnum;
import org.sosy_lab.ccvisu.graph.GraphData;
import org.sosy_lab.ccvisu.graph.GraphEdge;
import org.sosy_lab.ccvisu.graph.GraphVertex;
import org.sosy_lab.ccvisu.graph.GraphVertex.Shape;
import org.sosy_lab.ccvisu.graph.GraphicLayoutInfo;
import org.sosy_lab.ccvisu.graph.Group;
import org.sosy_lab.ccvisu.graph.NameVisibility;
import org.sosy_lab.ccvisu.graph.Position;
import org.sosy_lab.ccvisu.ui.FrameDisplay;
import org.sosy_lab.util.Colors;

/**
 * Writer for displaying the layout on the screen device.
 */
public class WriterDataLayoutDISP extends WriterDataLayout {

  private List<Set<String>>          xMap;
  private List<Set<String>>          yMap;
  private List<List<Set<GraphEdge>>> edgeMap;

  //write-color (computed once and stored)
  private Color                      frontColor;

  // Temporarily associated during callback from ScreenDisplay.
  private Graphics                   graphics;
  private int                        insetleft;
  private int                        insetbottom;
  private int                        xSize      = 0;
  private int                        ySize      = 0;

  // Ashgan added the following data field, which is used to
  // filter the display of vertices and edges
  private DisplayCriteria            dispFilter = null;

  private final FrameDisplay display;

  /**
   * Constructor.
   * @param graph       Graph representation, contains the positions of the vertices.
   */
  public WriterDataLayoutDISP(PrintWriter out, GraphData graph, Container frame, Options options) {
    super(out, graph, options);
    assert options.frame != null;

    // Adjust frontColor.
    adjustFrontColor();

    display = new FrameDisplay(this, frame);
    graph.addOnGraphChangeListener(display);

    if (display == null) {
      System.err.println("Runtime error: Could not open ScreenDisplay.");
      System.exit(1);
    }

    // Ashgan added the following line
    dispFilter = new DisplayCriteria(graph);
  }

  /**
   * Nothing to do here.
   * The constructor initializes the ScreenDisplay (frame and canvas),
   * and that calls back to the methods below
   * (writeDISP, writeLAY, toggleVertexNames, getVertexNames).
   */
  @Override
  public void write() {
  }

  /**
   * Writes the layout on the screen device (DISP output format).
   * Call-back method, invoked from <code>ScreenDisplay</code>.
   * @param size        Size of the output drawing area.
   * @param graphics    The drawing area of the canvas.
   * @param xCanvasSize  Width of the canvas.
   * @param yCanvasSize  Height of the canvas.
   * @param insetleft    Left inset of the drawing frame.
   * @param insetbottom  Bottom inset of the drawing frame.
   */
  public void writeDISP(Dimension size, Graphics graphics, int xCanvasSize,
      int yCanvasSize, int insetleft, int insetbottom) {

    this.graphics = graphics;
    this.insetbottom = insetbottom;
    this.insetleft = insetleft;

    // Maps for getting the vertices at mouse positions.
    xMap = new ArrayList<Set<String>>(xCanvasSize);
    yMap = new ArrayList<Set<String>>(yCanvasSize);

    for (int i = 0; i < xCanvasSize; ++i) {
      xMap.add(new TreeSet<String>());
    }

    for (int i = 0; i < yCanvasSize; ++i) {
      yMap.add(new TreeSet<String>());
    }

    // Maps for getting the edges at mouse positions.
    if (options.getOption(OptionsEnum.showEdges).getBool()) {
      if (xSize == xCanvasSize && ySize == yCanvasSize) {
        for (int x = 0; x < xCanvasSize; ++x) {
          List<Set<GraphEdge>> lVec = edgeMap.get(x);
          for (int y = 0; y < yCanvasSize; ++y) {
            lVec.get(y).clear();
          }
        }
      } else {
        edgeMap = new ArrayList<List<Set<GraphEdge>>>(xCanvasSize);
        for (int x = 0; x < xCanvasSize; ++x) {
          List<Set<GraphEdge>> lVec = new ArrayList<Set<GraphEdge>>(yCanvasSize);
          edgeMap.add(lVec);
          for (int y = 0; y < yCanvasSize; ++y) {
            lVec.add(new TreeSet<GraphEdge>());
          }
        }
        xSize = xCanvasSize;
        ySize = yCanvasSize;
      }
    }

    List<GraphVertex> emptyVertices = new ArrayList<GraphVertex>();
    List<GraphEdge> emptyEdges = new ArrayList<GraphEdge>();

    // Write edges only
    writeGraphicsLayout(emptyVertices, graph.getEdges(), size);

    // Draw the vertices, group by group, groups on top of normal vertices.
    Iterator<Group> it = graph.getGroups().iterator();
    while (it.hasNext()) {
      Group group = it.next();
      if (group.isVisible()) {
        writeGraphicsLayout(group.getNodes(), emptyEdges, size);
      }
    }

    GraphicLayoutInfo graphicLayoutInfo = calculateOffsetAndScale(size);

    // Draw the group specific information (except default group)
    for (Group group : graph.getGroups()) {
      if (group == graph.getDefaultGroup()) {
        continue;
      }

      if (group.isVisible() && group.isDrawInfo()) {
        Position position = graphicLayoutInfo.mapToOriginal(new Position(group.getX(),
            group.getY(), 0));

        int x = (int) (position.x + insetleft);
        int y = (int) (position.y - insetbottom);

        int left = x - 5;
        int right = x + 5;
        int top = y - 5;
        int bottom = y + 5;

        graphics.setColor(Color.BLACK);
        graphics.drawLine(left, top, right, bottom);
        graphics.drawLine(right, top, left, bottom);

        graphics.setColor(group.getColor());
        graphics.drawLine(left, y, right, y);
        graphics.drawLine(x, top, x, bottom);

        int radius = (int) (group.getAverageRadius()
            * graphicLayoutInfo.getScale().x);
        int diam = (radius + radius);
        graphics.drawOval(x - radius, y - radius, diam, diam);
        graphics.setColor(Color.BLACK);
      }
    }

    if (options.writerDataGraphicsDISPListener != null) {
      options.writerDataGraphicsDISPListener.onUpdateImage(size,
          graphicLayoutInfo, graphics);
    }
  }

  /**
   * Writes a vertex on screen.
   * @param vertex  The vertex object, to access vertex attributes.
   * @param xPos       x coordinate of the vertex.
   * @param yPos       y coordinate of the vertex.
   * @param zPos       z coordinate of the vertex.
   * @param Width      Width of the vertex.
   * @param height     Height of the vertex.
   */
  @Override
  public void writeVertex(GraphVertex vertex, int xPos, int yPos, int zPos,
                          int width, int height, Color color) {

    assert (vertex.isShowVertex());
    assert (!vertex.isAuxiliary());

    // Correction for inset.left and inset.bottom
    xPos = xPos + insetleft;
    yPos = yPos - insetbottom;

    int startX = xPos - width;
    int startY = yPos - height;

    // Draw the vertex.
    int radius = width;
    int diam = 2 * width;
    graphics.setColor(color);

    if ((vertex.getShape() == Shape.BOX)
        || (vertex.getShape() == Shape.FIXED_SIZE_BOX)) {
      // BOX
      graphics.fillRect(startX, startY, diam, diam);
      if (options.getOption(OptionsEnum.blackCircle).getBool()) {
        graphics.setColor(Colors.get(options.getOption(OptionsEnum.ringColor).getString()));
        graphics.drawRect(startX, startY, diam, diam);
      }

    } else if (vertex.getShape() == Shape.DISC) {
      // DISC
      graphics.fillOval(startX, startY, diam, diam);
      if (options.getOption(OptionsEnum.blackCircle).getBool()) {
        graphics.setColor(Colors.get(options.getOption(OptionsEnum.ringColor).getString()));
        graphics.drawOval(startX, startY, diam, diam);
      }
      graphics.setColor(frontColor);

    } else if (vertex.getShape() == Shape.RBOX) {
      // RBox
      float d = diam;
      graphics.fillRoundRect(startX, startY, diam, diam,
          (int) (3 * d / 4), (int) (3 * d / 4));

      // graphics.fillPolygon(xs, ys, 8);
      if (options.getOption(OptionsEnum.blackCircle).getBool()) {
        graphics.setColor(Colors.get(options.getOption(OptionsEnum.ringColor).getString()));
        // graphics.drawPolygon(xs, ys, 8);
      }

    } else if (vertex.getShape() == Shape.FILLER_RECT) {
      // FILLER_RECT
      graphics.fillRect(startX, startY, height, width);
      if (options.getOption(OptionsEnum.blackCircle).getBool()) {
        graphics.setColor(Colors.get(options.getOption(OptionsEnum.ringColor).getString()));
        graphics.drawRect(startX, startY, height, width);
      }

    } else {
      // UNKNOWN
      assert (false);
    }

    // Draw a little loop if the vertex has a self-loop.
    if (vertex.hasSelfLoop() && options.getOption(OptionsEnum.showEdges).getBool()) {
      graphics.drawOval(startX, startY, (int) ((1.8) * diam),
          (int) ((1.8) * diam + 4));
    }

    if (vertex.isShowName()) {
      // Draw annotation.
      // Use inverted background color for the annotation.
      graphics.setColor(frontColor);
      String vertexLabel = vertex.getLabel();
      if (options.getOption(OptionsEnum.shortNames).getBool()) {
        vertexLabel = NameHandler.shortenName(vertexLabel);
      }
      graphics.drawString(vertexLabel, xPos + radius + 3, yPos + 3);
      /*
       * if (options.getOption(OptionsEnum.enableFeatureVisu).getBool()) {
       *   int res = Statistics.calculateProximityDegreeExtended(curVertex, graph);
       *   String resStr = "(proximity degree: " + res + ")";
       *   int lYPos = yPos + 3 + options.getOption(OptionsEnum.fontSize).getInt();
       *   mGraphics.drawString(resStr, xPos + radius + 3, lYPos);
       * }
       */
    }

    try {
      // For interactive annotation: Store vertex names at their positions in the maps.
      int endX = Math.min(xPos + radius, xMap.size() - 1);
      for (int pos = Math.max(startX, 0); pos <= endX; ++pos) {
        xMap.get(pos).add(vertex.getName());
      }

      int endY = Math.min(yPos + radius, yMap.size() - 1);
      for (int pos = Math.max(startY, 0); pos <= endY; ++pos) {
        yMap.get(pos).add(vertex.getName());
      }
    } catch (Exception e) {
      // Tolerate exception (it's because of the concurrent thread).
    }
  }

  /**
   * Writes an edge.
   * @param edge        an edge in graph.edges
   * @param xPos1       x coordinate of the first point.
   * @param yPos1       y coordinate of the first point.
   * @param zPos1       z coordinate of the first point.
   * @param xPos2       x coordinate of the second point.
   * @param yPos2       y coordinate of the second point.
   * @param zPos2       z coordinate of the second point.
   */
  @Override
  public void writeEdge(GraphEdge edge, int xPos1, int yPos1, int zPos1,
      int xPos2, int yPos2, int zPos2) {

    assert (!edge.isAuxiliaryEdge());

    // reflexive edges are not allowed by specification
    if (xPos1 == xPos2 && yPos1 == yPos2) { return; }

    // Correction for inset.left
    xPos1 = xPos1 + insetleft;
    xPos2 = xPos2 + insetleft;

    // Correction for inset.bottom
    yPos1 = yPos1 - insetbottom;
    yPos2 = yPos2 - insetbottom;

    // Draw
    graphics.setColor(edge.getColor());
    paintArrow(graphics, xPos1, yPos1, xPos2, yPos2, edge.isDashed());

    // Draw the annotation
    if (edge.showName()) {
      int xPos = (xPos1 + xPos2 + options.getOption(OptionsEnum.fontSize).getInt()) / 2;
      int yPos = (yPos1 + yPos2 + options.getOption(OptionsEnum.fontSize).getInt()) / 2;
      graphics.drawString(edge.getRelName(), xPos, yPos);
    }

    // For interactive annotation: Store edges at their positions in the maps.
    // Using Bresenham's line algorithm.
    boolean steep = Math.abs(yPos2 - yPos1) > Math.abs(xPos2 - xPos1);
    if (steep) {
      int tmp = xPos1;
      xPos1 = yPos1;
      yPos1 = tmp;
      tmp = xPos2;
      xPos2 = yPos2;
      yPos2 = tmp;
    }

    if (xPos1 > xPos2) {
      int tmp = xPos1;
      xPos1 = xPos2;
      xPos2 = tmp;
      tmp = yPos1;
      yPos1 = yPos2;
      yPos2 = tmp;
    }

    int deltaX = xPos2 - xPos1;
    int deltaY = Math.abs(yPos2 - yPos1);
    float error = 0;
    float deltaError = ((float) deltaY) / deltaX;
    int y = yPos1;
    int ystep;

    if (yPos1 < yPos2) {
      ystep = 1;
    } else {
      ystep = -1;
    }

    for (int x = xPos1; x <= xPos2; x++) {
      try {
        if (steep) {
          if (y >= 0 && y < xSize && x >= 0 && x < ySize) {
            edgeMap.get(y).get(x).add(edge);
          }
        } else {
          if (x >= 0 && x < xSize && y >= 0 && y < ySize) {
            edgeMap.get(x).get(y).add(edge);
          }
        }
        error += deltaError;
        if (error >= 0.5) {
          y += ystep;
          error -= 1.0;
        }
      } catch (Exception e) {
        // Tolerate exception (it's because of the concurrent thread).
      }
    }
  }

  /**
   * Marks all vertices whose node names match the given regular expression.
   * Call-back method, invoked from within ScreenDisplay.
   * @param regEx     Regular expression.
   */
  public void markVertices(String regEx) {
    for (GraphVertex vertex : graph.getVertices()) {
      if (vertex.getName().matches(regEx)) {
        vertex.setColor(Color.red);
        vertex.setShowName(NameVisibility.Priority.FIND, true);
      } else {
        vertex.resetColor();
        vertex.unsetShowName(NameVisibility.Priority.FIND);
      }
    }
  }

  /**
   * Resets all marked vertices.
   */
  public void resetMarkedVertices() {
    for (GraphVertex vertex : graph.getVertices()) {
      vertex.resetColor();
      vertex.unsetShowName(NameVisibility.Priority.FIND);
    }
  }

  /**
   * Toggle the showName flag of the vertices and edges at the given position.
   * Call-back method, invoked from within ScreenDisplay.
   * @param point       coordinates of the vertex.
   * @return number of names toggled
   */
  public int toggleNames(Point point) {
    int xPos = (int) point.getX();
    int yPos = (int) point.getY();
    int nb = 0;

    Set<String> tmp = new TreeSet<String>(xMap.get(xPos));
    tmp.retainAll(yMap.get(yPos));

    for (String name : tmp) {
      nb++;
      GraphVertex vertex = graph.getVertexByName(name);
      vertex.setShowName(NameVisibility.Priority.MANUAL, !vertex.isShowName());
    }

    // edges
    if (options.getOption(OptionsEnum.showEdges).getBool()) {
      Set<GraphEdge> edgesIndex = edgeMap.get(xPos).get(yPos);
      for (GraphEdge edge : edgesIndex) {
        nb++;
        edge.setShowName(!edge.showName());
      }
    }

    return nb;
  }

  /**
   * Compute list of names of the vertices and edges at the given position.
   * Call-back method, invoked from within ScreenDisplay.
   * @param point       coordinates.
   */
  public Set<String> getNames(Point point, Boolean includeEdges) {
    int xPos = (int) point.getX();
    int yPos = (int) point.getY();
    Set<String> tmp = new TreeSet<String>();

    try {
      if (xPos >= xMap.size() || yPos >= yMap.size()) {
        return tmp;
      }

      tmp.addAll(xMap.get(xPos));
      tmp.retainAll(yMap.get(yPos));
    } catch (Exception e) {
      // Tolerate exception (it's because of the concurrent thread).
    }

    // Edges
    if ((includeEdges) && (options.getOption(OptionsEnum.showEdges).getBool())) {
      Set<GraphEdge> edgesIndex = edgeMap.get(xPos).get(yPos);
      for (GraphEdge edge : edgesIndex) {
        tmp.add(edge.getRelName());
      }
    }

    return tmp;
  }

  /**
   * Restrict the set of vertices displayed on the screen to
   * the vertices within the given rectangular (i.e., zoom).
   * Call-back method, invoked from within ScreenDisplay.
   * @param topLeft      coordinates of the top left corner of the rectangular.
   * @param bottomRight  coordinates of the bottom right corner of the rectangular.
   */
  public void restrictShowedVertices(Point topLeft, Point bottomRight) {

    Set<String> xNodes = new TreeSet<String>();
    int end = (int) Math.min(bottomRight.getX(), xMap.size());
    int minX = Math.max(0, (int) topLeft.getX());

    for (int i = minX; i < end; i++) {
      xNodes.addAll(xMap.get(i));
    }

    end = (int) Math.min(bottomRight.getY(), yMap.size());
    int minY = Math.max(0, (int) topLeft.getY());
    Set<String> yNodes = new TreeSet<String>();

    for (int i = minY; i < end; i++) {
      yNodes.addAll(yMap.get(i));
    }

    Set<String> nodesToKeep = xNodes;
    nodesToKeep.retainAll(yNodes);

    for (int i = 0; i < graph.getVertices().size(); i++) {
      GraphVertex vertex = graph.getVertices().get(i);
      if (!nodesToKeep.contains(vertex.getName())) {
        vertex.setShowVertex(false);
      }
    }
    // Ashgan starts
    // Every time the canvas view changes, the
    // undo list in the associated display filter instance
    // should also be updated
    this.dispFilter.resetUndoList(graph.getVertices());
    // Ashgan ends
  }

  /**
   * Reset vertex restriction that was set by restrictShowedVertices.
   * Call-back method, invoked from within ScreenDisplay.
   */
  public void resetRestriction() {
    // Handle vertex options.
    for (GraphVertex vertex : graph.getVertices()) {
      // hideSource (do not show vertex if it is source of an edge).
      if (options.getOption(OptionsEnum.hideSource).getBool() && vertex.isSource()) {
        vertex.setShowVertex(false);
      } else {
        vertex.setShowVertex(true);
      }
    }
    // Ashgan starts
    // Every time the canvas view changes, the
    // undo list in the associated display filter instance
    // should also be updated
    this.dispFilter.resetUndoList(graph.getVertices());
    // Ashgan ends
  }

  /**
   * Gets the local graph representation (layout).
   * Call-back method, invoked from within ScreenDisplay.
   * @return   Graph/layout representation to switch to.
   */
  public GraphData getGraphData() {
    return graph;
  }

  /**
   * adjust frontColor
   */
  public void adjustFrontColor() {
    Color backColor = options.backColor.get();
    frontColor = new Color(0xffffffff - backColor.getRGB());
    //problem when using gray: colors too close => hard to read
    //ignore alpha
    if (Math.abs(frontColor.getRed() - backColor.getRed()) < 10
        && Math.abs(frontColor.getBlue() - backColor.getBlue()) < 10
        && Math.abs(frontColor.getGreen() - backColor.getGreen()) < 10) {
      frontColor = Color.BLACK;
    }
  }

  /**
   * the color of the text
   * @return the color of the text
   */
  public Color getWriteColor() {
    return frontColor;
  }

  /**
   * Open the name of what is under the cursor as if it is an URL.
   * @param point   Coordinates
   */
  public void openURL(Point point) {
    // TODO: Simplify!
    String targets = getNames(point, false).toString();
    int lenght = targets.length();

    if (lenght <= 2) {
      return;
    }

    StringTokenizer stringTokenizer = new StringTokenizer(targets.substring(1, lenght - 1));
    if (stringTokenizer.hasMoreTokens()) {
      String url = stringTokenizer.nextToken();

      if ((url.length() > 0 && url.charAt(0) == '"') && url.endsWith("\"")) {
        url = url.substring(1, url.length() - 1);
      }

      if (options.getOption(OptionsEnum.browser).getString().equals("")) {
        if (!guessBrowser(url)) {
          System.err.println("Unable to find browser");
          return;
        }
      } else {
        String cmd[] = { options.getOption(OptionsEnum.browser).getString(), url };

        System.err.println("opening: " + url);

        try {
          Runtime rt = Runtime.getRuntime();
          rt.exec(cmd);
        } catch (Throwable t) {
          t.printStackTrace();
        }
      }
    }
  }

  private boolean guessBrowser(String url) {

    String[] possibility = { "firefox", "mozilla", "opera", "safari",
                             "iexplorer", "epiphany", "konqueror" };

    for (int i = 0; i < possibility.length; i++) {
      options.getOption(OptionsEnum.browser).set(possibility[i]);

      String cmd[] = { options.getOption(OptionsEnum.browser).getString(), url };

      try {
        Runtime rt = Runtime.getRuntime();
        rt.exec(cmd);
        return true;
      } catch (Throwable t) {
      }
    }
    options.getOption(OptionsEnum.browser).set("");
    return false;
  }

  /**
   * Draws an arrow from one 2d-coordinate to another.
   */
  private void paintArrow(Graphics g, int x0, int y0, int x1, int y1,
      boolean dashed) {

    if (dashed) {
      Stroke drawingStroke = new BasicStroke(1, BasicStroke.CAP_BUTT,
          BasicStroke.JOIN_BEVEL, 0, new float[] { 9 }, 0);

      Graphics2D g2d = (Graphics2D) g;
      Stroke oldStroke = g2d.getStroke();
      g2d.setStroke(drawingStroke);

      // Draw line.
      g2d.drawLine(x0, y0, x1, y1);

      g2d.setStroke(oldStroke);
    } else {
      g.drawLine(x0, y0, x1, y1);
    }

    // Draw arrow head.
    int[] aps = paintArrow(x0, y0, x1, y1);
    g.drawLine(aps[0], aps[1], aps[2], aps[3]);
    g.drawLine(aps[4], aps[5], aps[6], aps[7]);
  }

  /**
   * @return the display
   */
  public FrameDisplay getDisplay() {
    return display;
  }

  // Ashgan starts
  /**
   * @param filterType <br>
   * 0: Display unchanged vertices<br>
   * 1: Display removed vertices<br>
   * 2: Display added vertices
   */
  public void filterVertices(int filterType) {
    dispFilter.filterVertices(graph, filterType, true);
  }

  /**
   * @param filterType <br>
   * 0: Display unchanged edges<br>
   * 1: Display removed edges<br>
   * 2: Display added edges
   */
  public void filterEdges(int filterType) {
    dispFilter.filterEdges(graph, filterType, true);
  }

  public void filterOff() {
    dispFilter.resetView(graph.getVertices());
  }
  // Ashgan ends
}
